﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using Notoric.Model.Collections;
using Notoric.Model.Internal;
using Notoric.Model.Packaging;
using Notoric.Model.Validation;

namespace Notoric.Model.Elements
{
    /// <summary>
    /// Represents one beat of a measure.
    /// </summary>
    public class BeatElement : ModelElement, IComparable<BeatElement>
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="BeatElement"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="CompositionPart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="MusicalContentElement"/>
        /// </para>
        /// <para>
        /// The musical content element that directly owns this beat.
        /// </para>
        /// </param>
        /// <param name="beginning">
        /// <para>
        /// Type: <see cref="System.UInt16"/>
        /// </para>
        /// <para>
        /// The beginning of the beat.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="packagePart"/> or <paramref name="owner"/> is <see langword="null"/>.
        /// </exception>
        internal BeatElement(CompositionPart packagePart, MusicalContentElement owner, ushort beginning)
            : base(packagePart, owner)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");

            Beginning = beginning;

            symbolsCollection = new ElementCollection<DurationalSymbolElement>(this, symbolsElement);
            symbols = new ReadOnlyCollection<DurationalSymbolElement>(symbolsCollection);

            attachmentsCollection = new ElementCollection<AttachmentElement>(this, attachmentsElement);
            attachments = new ReadOnlyCollection<AttachmentElement>(attachmentsCollection);
        }

        /// <summary>
        /// Creates a new <see cref="BeatElement"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="CompositionPart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="MusicalContentElement"/>
        /// </para>
        /// <para>
        /// The musical content element that directly owns this beat.
        /// </para>
        /// </param>
        /// <param name="element">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that holds the data of this object.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        internal BeatElement(CompositionPart packagePart, MusicalContentElement owner, XmlElement element)
            : base(packagePart, owner, element)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");

            beginningAttribute = element.Attributes[PackageVocabulary.BeginningAttribute];
            symbolsElement = element[PackageVocabulary.DurationalSymbolElementName + "s", PackageVocabulary.PackageNamespaceUri];
            attachmentsElement = element[PackageVocabulary.AttachmentElementName + "s", PackageVocabulary.PackageNamespaceUri];

            symbolsCollection = new ElementCollection<DurationalSymbolElement>(this, symbolsElement);
            symbols = new ReadOnlyCollection<DurationalSymbolElement>(symbolsCollection);

            attachmentsCollection = new ElementCollection<AttachmentElement>(this, attachmentsElement);
            attachments = new ReadOnlyCollection<AttachmentElement>(attachmentsCollection);
        }

        #endregion

        #region ModelElement Members

        /// <inheritdoc/>
        protected override XmlElement CreateObjectElement(XmlDocument document)
        {
            var element = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.BeatElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            beginningAttribute = document.CreateAttribute(PackageVocabulary.BeginningAttribute);
            symbolsElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.DurationalSymbolElementName + "s",
                PackageVocabulary.PackageNamespaceUri
                );
            attachmentsElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.AttachmentElementName + "s",
                PackageVocabulary.PackageNamespaceUri
                );

            element.Attributes.Append(beginningAttribute);
            element.AppendChild(symbolsElement);
            element.AppendChild(attachmentsElement);

            return element;
        }

        #endregion

        #region IComparable<BeatElement> Members

        /// <inheritdoc/>
        public int CompareTo(BeatElement other)
        {
            return Beginning - other.Beginning;
        }

        #endregion

        #region Internal Methods

        /// <summary>
        /// Adds a given attachemnt to the <see cref="Attachments"/> collection.
        /// </summary>
        /// <param name="attachment">
        /// <para>
        /// Type: <see cref="AttachmentElement"/>
        /// </para>
        /// <para>
        /// The attachment.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="attachment"/> is <see langword="null"/>.
        /// </exception>
        internal void AddAttachment(AttachmentElement attachment)
        {
            if (attachment == null)
                throw new ArgumentNullException("attachment");

            (PackagePart as CompositionPart).RaiseGlobalChangePending(FindOwner<MeasureElement>(), FindOwner<MeasureElement>());

            if (attachment.FindOwner<BeatElement>() != this)
                attachment.MigrateTo(this);

            attachmentsCollection.Add(attachment);
        }

        /// <summary>
        /// Adds a new <see cref="ChordElement"/> to this beat.
        /// </summary>
        /// <param name="voice">
        /// <para>
        /// Type: <see cref="VoiceDescriptorElement"/>
        /// </para>
        /// <para>
        /// The voice to which this chord is being inserted.
        /// </para>
        /// </param>
        /// <param name="duration">
        /// <para>
        /// Type: <see cref="UInt16"/>
        /// </para>
        /// <para>
        /// The duration of the new chord.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="ChordElement"/>
        /// </para>
        /// <para>
        /// The created chord.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentException">
        /// Another symbol already exists at this beat in <paramref name="voice"/>.
        /// </exception>
        /// <exception cref="ValidationException">
        /// The <paramref name="duration"/> or <paramref name="voice"/> is invalid.
        /// </exception>
        internal ChordElement AddChord(VoiceDescriptorElement voice, ushort duration)
        {
            if (this[voice] != null)
                throw new ArgumentException("There already exists another symbol that starts at this beat in voice " + voice + "!", "voice");

            (PackagePart as CompositionPart).RaiseGlobalChangePending(FindOwner<MeasureElement>(), FindOwner<MeasureElement>());

            var chord = new ChordElement((CompositionPart)PackagePart, this, voice, duration);
            symbolsCollection.Add(chord);

            return chord;
        }

        /// <summary>
        /// Adds an existing symbol to this beat.
        /// </summary>
        /// <remarks>
        /// The method automatically calls the <see cref="ModelElement.MigrateTo"/> method on the added <paramref name="symbol"/>.
        /// </remarks>
        /// <param name="symbol">
        /// <para>
        /// Type: <see cref="DurationalSymbolElement"/>
        /// </para>
        /// <para>
        /// The added symbol.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="symbol"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// Another symbol already exists at this beat in the same voice.
        /// </exception>
        internal void AddExistingSymbol(DurationalSymbolElement symbol)
        {
            if (symbol == null)
                throw new ArgumentNullException("symbol");
            if (this[symbol.Voice] != null)
                throw new ArgumentException("There already exists another symbol that starts at this beat in voice " + symbol.Voice.Number + "!", "symbol");

            (PackagePart as CompositionPart).RaiseGlobalChangePending(FindOwner<MeasureElement>(), FindOwner<MeasureElement>());

            symbol.MigrateTo(this);
            symbolsCollection.Add(symbol);
        }

        /// <summary>
        /// Adds a new <see cref="RestElement"/> to this beat.
        /// </summary>
        /// <param name="voice">
        /// <para>
        /// Type: <see cref="VoiceDescriptorElement"/>
        /// </para>
        /// <para>
        /// The voice to which this rest is being inserted.
        /// </para>
        /// </param>
        /// <param name="duration">
        /// <para>
        /// Type: <see cref="UInt16"/>
        /// </para>
        /// <para>
        /// The duration of the new rest.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="RestElement"/>
        /// </para>
        /// <para>
        /// The created rest.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentException">
        /// Another symbol already exists at this beat in <paramref name="voice"/>.
        /// </exception>
        /// <exception cref="ValidationException">
        /// The <paramref name="duration"/> or <paramref name="voice"/> is invalid.
        /// </exception>
        internal RestElement AddRest(VoiceDescriptorElement voice, ushort duration)
        {
            if (this[voice] != null)
                throw new ArgumentException("There already exists another symbol that starts at this beat in voice " + voice + "!", "voice");

            (PackagePart as CompositionPart).RaiseGlobalChangePending(FindOwner<MeasureElement>(), FindOwner<MeasureElement>());

            var rest = new RestElement((CompositionPart)PackagePart, this, voice, duration);
            symbolsCollection.Add(rest);

            return rest;
        }

        /// <summary>
        /// Removes a given attachment from the <see cref="Attachments"/> collection.
        /// </summary>
        /// <param name="attachment">
        /// <para>
        /// Type: <see cref="AttachmentElement"/>
        /// </para>
        /// <para>
        /// The removed attachment.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="attachment"/> is <see langword="null"/>.
        /// </exception>
        internal void RemoveAttachment(AttachmentElement attachment)
        {
            if (attachment == null)
                throw new ArgumentNullException("attachment");
            
            (PackagePart as CompositionPart).RaiseGlobalChangePending(FindOwner<MeasureElement>(), FindOwner<MeasureElement>());

            attachmentsCollection.Remove(attachment);
        }

        /// <summary>
        /// Removes a given symbol from the <see cref="Symbols"/> collection.
        /// </summary>
        /// <param name="symbol">
        /// <para>
        /// Type: <see cref="DurationalSymbolElement"/>
        /// </para>
        /// <para>
        /// The removed symbol.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="symbol"/> is <see langword="null"/>.
        /// </exception>
        internal void RemoveSymbol(DurationalSymbolElement symbol)
        {
            if (symbol == null)
                throw new ArgumentNullException("symbol");

            (PackagePart as CompositionPart).RaiseGlobalChangePending(FindOwner<MeasureElement>(), FindOwner<MeasureElement>());

            var composition = FindOwner<CompositionElement>();
            var toRemove = new List<ConnectorElement>();
            foreach (var connector in symbol.Connectors)
                toRemove.Add(connector);
            foreach (var connector in toRemove)
                composition.Connectors.Remove(connector);

            symbolsCollection.Remove(symbol);
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the symbol that begins at this beat and belongs to a given voice.
        /// </summary>
        /// <param name="voice">
        /// <para>
        /// Type: <see cref="System.Byte"/>
        /// </para>
        /// <para>
        /// The number of the voice whose symbol is being requested.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="DurationalSymbolElement"/>
        /// </para>
        /// <para>
        /// A reference to the symbol that satisfies the condition if found, <see langword="null"/> othewise.
        /// </para>
        /// </returns>
        public DurationalSymbolElement this[byte voice]
        {
            get { return symbolsCollection.FirstOrDefault(s => s.Voice.Number == voice); }
        }

        /// <summary>
        /// Gets the symbol that begins at this beat and belongs to a given voice.
        /// </summary>
        /// <param name="voice">
        /// <para>
        /// Type: <see cref="VoiceDescriptorElement"/>
        /// </para>
        /// <para>
        /// The descriptor of the voice whose symbol is being requested.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="DurationalSymbolElement"/>
        /// </para>
        /// <para>
        /// A reference to the symbol that satisfies the condition if found, <see langword="null"/> othewise.
        /// </para>
        /// </returns>
        public DurationalSymbolElement this[VoiceDescriptorElement voice]
        {
            get { return symbolsCollection.FirstOrDefault(s => s.Voice == voice); }
        }

        /// <summary>
        /// Gets the collection of attachments at this beat.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ReadOnlyCollection{T}"/> of <see cref="AttachmentElement"/>
        /// </para>
        /// <para>
        /// The collection of attachments at this beat.
        /// </para>
        /// </value>
        public ReadOnlyCollection<AttachmentElement> Attachments
        {
            get { return attachments; }
        }

        /// <summary>
        /// Gets or sets the beginning of this beat.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="System.UInt16"/>
        /// </para>
        /// <para>
        /// The beginning defines when this beat starts (what real beat it represents).
        /// </para>
        /// <para>
        /// This value is given in multiples of 128-th beats and is relative to the owner <see cref="MeasureElement"/>.<br/>
        /// <b>Examples:</b><br/>
        /// 0 stands for the first beat of the measure<br/>
        /// 32 stands for the second beat of the measure (1 quarter note, see <see cref="DurationalSymbolElement.Quarter"/>)<br/>
        /// </para>
        /// </value>
        public ushort Beginning
        {
            get { return ushort.Parse(beginningAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat); }
            internal set
            {
                (PackagePart as CompositionPart).RaiseGlobalChangePending(FindOwner<MeasureElement>(), FindOwner<MeasureElement>());
                beginningAttribute.Value = value.ToString();

                RaisePropertyChanged("Beginning");
            }
        }

        /// <summary>
        /// Gets the collection of durational symbols at this beat.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ReadOnlyCollection{T}"/> of <see cref="DurationalSymbolElement"/>
        /// </para>
        /// <para>
        /// The durational symbols (e.g. notes, rests) from all the voices of the owner part
        /// that begins at this beat.
        /// </para>
        /// </value>
        public ReadOnlyCollection<DurationalSymbolElement> Symbols
        {
            get { return symbols; }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the value of the <see cref="Attachments"/> property.
        /// </summary>
        private ReadOnlyCollection<AttachmentElement> attachments;
        /// <summary>
        /// Holds the collection of attachments at this beat.
        /// </summary>
        private ElementCollection<AttachmentElement> attachmentsCollection;
        /// <summary>
        /// Holds the XML element that holds the <see cref="attachmentsCollection"/>.
        /// </summary>
        private XmlElement attachmentsElement;
        /// <summary>
        /// Holds the XML attribute that holds the value of the <see cref="Beginning"/> property.
        /// </summary>
        private XmlAttribute beginningAttribute;
        /// <summary>
        /// Holds the value of the <see cref="Symbols"/> property.
        /// </summary>
        private ReadOnlyCollection<DurationalSymbolElement> symbols;
        /// <summary>
        /// Holds the collection of durational symbols at this beat.
        /// </summary>
        private ElementCollection<DurationalSymbolElement> symbolsCollection;
        /// <summary>
        /// Holds the XML element that holds the <see cref="symbolsCollection"/>.
        /// </summary>
        private XmlElement symbolsElement;

        #endregion
    }
}
